<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
	<meta name="keywords" content="Seata、数据源、数据源代理、多数据源" />
	<meta name="description" content="本文主要介绍Seata数据源代理实现原理及使用时可能会遇到的问题" />
	<!-- 网页标签标题 -->
	<title>Seata数据源代理解析</title>
  <link rel="shortcut icon" href="/img/seata_logo_small.jpeg"/>
	<link rel="stylesheet" href="/build/blogDetail.css" />
</head>
<body>
	<div id="root"><div class="blog-detail-page" data-reactroot=""><header class="header-container header-container-normal"><div class="header-body"><a href="/zh-cn/index.html"><img class="logo" src="//img.alicdn.com/tfs/TB1gqL1w4D1gK0jSZFyXXciOVXa-1497-401.png"/></a><div class="search search-normal"><span class="icon-search"></span></div><span class="language-switch language-switch-normal">En</span><div class="header-menu"><img class="header-menu-toggle" src="https://img.alicdn.com/tfs/TB14eEmw7P2gK0jSZPxXXacQpXa-38-32.png"/><ul><li class="menu-item menu-item-normal"><a href="/zh-cn/index.html" target="_self">首页</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/docs/overview/what-is-seata.html" target="_self">文档</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/docs/developers/developers_dev.html" target="_self">开发者</a></li><li class="menu-item menu-item-normal menu-item-normal-active"><a href="/zh-cn/blog/index.html" target="_self">博客</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/community/index.html" target="_self">社区</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/blog/download.html" target="_self">下载</a></li></ul></div></div></header><section class="blog-content markdown-body"><h1>数据源代理</h1>
<p>在Seata1.3.0版本中，数据源自动代理和手动代理一定不能混合使用，否则会导致多层代理，从而导致以下问题：</p>
<ol>
<li>单数据源情况下：导致分支事务提交时，undo_log本身也被代理，即<code>为 undo_log 生成了 undo_log， 假设为undo_log2</code>，此时undo_log将被当作分支事务来处理；分支事务回滚时，因为<code>undo_log2</code>生成的有问题，在<code>undo_log对应的事务分支</code>回滚时会将<code>业务表关联的undo_log</code>也一起删除，从而导致<code>业务表对应的事务分支</code>回滚时发现undo_log不存在，从而又多生成一条状态为1的undo_log。这时候整体逻辑已经乱了，很严重的问题</li>
<li>多数据源和<code>逻辑数据源被代理</code>情况下：除了单数据源情况下会出现的问题，还可能会造成死锁问题。死锁的原因就是针对undo_log的操作，本该在一个事务中执行的<code>select for update</code> 和 <code>delete</code> 操作，被分散在多个事务中执行，导致一个事务在执行完<code>select for update</code>后一直不提交，一个事务在执行<code>delete</code>时一直等待锁，直到超时</li>
</ol>
<h2>代理描述</h2>
<p>即对DataSource代理一层，重写一些方法。比如<code>getConnection</code>方法，这时不直接返回一个<code>Connection</code>，而是返回<code>ConnectionProxy</code>，其它的以此类推</p>
<pre><code class="language-java"><span class="hljs-comment">// DataSourceProxy</span>

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">DataSourceProxy</span><span class="hljs-params">(DataSource targetDataSource)</span> </span>{
    <span class="hljs-keyword">this</span>(targetDataSource, DEFAULT_RESOURCE_GROUP_ID);
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">init</span><span class="hljs-params">(DataSource dataSource, String resourceGroupId)</span> </span>{
    DefaultResourceManager.get().registerResource(<span class="hljs-keyword">this</span>);
}

<span class="hljs-function"><span class="hljs-keyword">public</span> Connection <span class="hljs-title">getPlainConnection</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> SQLException </span>{
    <span class="hljs-keyword">return</span> targetDataSource.getConnection();
}

<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> ConnectionProxy <span class="hljs-title">getConnection</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> SQLException </span>{
    Connection targetConnection = targetDataSource.getConnection();
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ConnectionProxy(<span class="hljs-keyword">this</span>, targetConnection);
}
</code></pre>
<h2>手动代理</h2>
<p>即手动注入一个<code>DataSourceProxy</code>，如下</p>
<pre><code>@Bean
public DataSource druidDataSource() {
    return new DruidDataSource()
}

@Primary
@Bean(&quot;dataSource&quot;)
public DataSourceProxy dataSource(DataSource druidDataSource) {
    return new DataSourceProxy(druidDataSource);
}
</code></pre>
<h2>自动代理</h2>
<p>针对<code>DataSource</code>创建一个代理类，在代理类里面基于<code>DataSource</code>获取<code>DataSourceProxy(如果没有就创建)</code>，然后调用<code>DataSourceProxy</code>的相关方法。核心逻辑在<code>SeataAutoDataSourceProxyCreator</code>中</p>
<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SeataAutoDataSourceProxyCreator</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractAutoProxyCreator</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger LOGGER = LoggerFactory.getLogger(SeataAutoDataSourceProxyCreator.class);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String[] excludes;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Advisor advisor = <span class="hljs-keyword">new</span> DefaultIntroductionAdvisor(<span class="hljs-keyword">new</span> SeataAutoDataSourceProxyAdvice());

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">SeataAutoDataSourceProxyCreator</span><span class="hljs-params">(<span class="hljs-keyword">boolean</span> useJdkProxy, String[] excludes)</span> </span>{
        <span class="hljs-keyword">this</span>.excludes = excludes;
        setProxyTargetClass(!useJdkProxy);
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-keyword">protected</span> Object[] getAdvicesAndAdvisorsForBean(Class&lt;?&gt; beanClass, String beanName, TargetSource customTargetSource) <span class="hljs-keyword">throws</span> BeansException {
        <span class="hljs-keyword">if</span> (LOGGER.isInfoEnabled()) {
            LOGGER.info(<span class="hljs-string">"Auto proxy of [{}]"</span>, beanName);
        }
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Object[]{advisor};
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">shouldSkip</span><span class="hljs-params">(Class&lt;?&gt; beanClass, String beanName)</span> </span>{
        <span class="hljs-keyword">return</span> SeataProxy.class.isAssignableFrom(beanClass) ||
                DataSourceProxy.class.isAssignableFrom(beanClass) ||
                !DataSource.class.isAssignableFrom(beanClass) ||
                Arrays.asList(excludes).contains(beanClass.getName());
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SeataAutoDataSourceProxyAdvice</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">MethodInterceptor</span>, <span class="hljs-title">IntroductionInfo</span> </span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">invoke</span><span class="hljs-params">(MethodInvocation invocation)</span> <span class="hljs-keyword">throws</span> Throwable </span>{
        DataSourceProxy dataSourceProxy = DataSourceProxyHolder.get().putDataSource((DataSource) invocation.getThis());
        Method method = invocation.getMethod();
        Object[] args = invocation.getArguments();
        Method m = BeanUtils.findDeclaredMethod(DataSourceProxy.class, method.getName(), method.getParameterTypes());
        <span class="hljs-keyword">if</span> (m != <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">return</span> m.invoke(dataSourceProxy, args);
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">return</span> invocation.proceed();
        }
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-keyword">public</span> Class&lt;?&gt;[] getInterfaces() {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Class[]{SeataProxy.class};
    }
}
</code></pre>
<h2>数据源多层代理</h2>
<pre><code>@Bean
@DependsOn(&quot;strangeAdapter&quot;)
public DataSource druidDataSource(StrangeAdapter strangeAdapter) {
    doxx
    return new DruidDataSource()
}

@Primary
@Bean(&quot;dataSource&quot;)
public DataSourceProxy dataSource(DataSource druidDataSource) {
    return new DataSourceProxy(druidDataSource);
}
</code></pre>
<ol>
<li>首先我们在配置类里面注入了两个<code>DataSource</code>，分别为： <code>DruidDataSource</code>和<code>DataSourceProxy</code>， 其中<code>DruidDataSource 作为 DataSourceProxy 的 targetDataSource 属性</code>，并且<code>DataSourceProxy</code>为使用了<code>@Primary</code>注解声明</li>
<li>应用默认开启了数据源自动代理，所以在调用<code>DruidDataSource</code>相关方法时，又会为为<code>DruidDataSource</code>创建一个对应的数据源代理<code>DataSourceProxy2</code></li>
<li>当我们在程序中想获取一个Connection时会发生什么？
<ol>
<li>先获取一个<code>DataSource</code>，因为<code>DataSourceProxy</code>为<code>Primary</code>，所以此时拿到的是<code>DataSourceProxy</code></li>
<li>基于<code>DataSource</code>获取一个<code>Connection</code>，即通过<code>DataSourceProxy</code>获取<code>Connection</code>。此时会先调用<code>targetDataSource 即 DruidDataSource 的 getConnection 方法</code>，但因为切面会对<code>DruidDataSource</code>进行拦截，根据步骤2的拦截逻辑可以知道，此时会自动创建一个<code>DataSourceProxy2</code>，然后调用<code>DataSourceProxy2#getConnection</code>，然后再调用<code>DruidDataSource#getConnection</code>。最终形成了双层代理， 返回的<code>Connection</code>也是一个双层的<code>ConnectionProxy</code></li>
</ol>
</li>
</ol>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ac0b91bd8fc4c48aa68afd5c58a42d5~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<p>上面其实是改造之后的代理逻辑，Seata默认的自动代理会对<code>DataSourceProxy</code>再次进行代理，后果就是代理多了一层此时对应的图如下</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/837aa0462d994e9a8614707c6a50b5ae~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<p>数据源多层代理会导致的两个问题在文章开头处已经总结了，下面会有案例介绍。</p>
<h1>分支事务提交</h1>
<p>通过<code>ConnectionProxy</code>中执行对应的方法，会发生什么？以update操作涉及到的一个分支事务提交为例：</p>
<ol>
<li>执行<code>ConnectionProxy#prepareStatement</code>， 返回一个<code>PreparedStatementProxy</code></li>
<li>执行<code>PreparedStatementProxy#executeUpdate</code>，<code>PreparedStatementProxy#executeUpdate</code>大概会帮做两件事情: 执行业务SQL和提交undo_log</li>
</ol>
<h2>提交业务SQL</h2>
<pre><code class="language-java"><span class="hljs-comment">// ExecuteTemplate#execute</span>
<span class="hljs-keyword">if</span> (sqlRecognizers.size() == <span class="hljs-number">1</span>) {
    SQLRecognizer sqlRecognizer = sqlRecognizers.get(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">switch</span> (sqlRecognizer.getSQLType()) {
        <span class="hljs-keyword">case</span> INSERT:
            executor = EnhancedServiceLoader.load(InsertExecutor.class, dbType,
                    <span class="hljs-keyword">new</span> Class[]{StatementProxy.class, StatementCallback.class, SQLRecognizer.class},
                    <span class="hljs-keyword">new</span> Object[]{statementProxy, statementCallback, sqlRecognizer});
            <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> UPDATE:
            executor = <span class="hljs-keyword">new</span> UpdateExecutor&lt;&gt;(statementProxy, statementCallback, sqlRecognizer);
            <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> DELETE:
            executor = <span class="hljs-keyword">new</span> DeleteExecutor&lt;&gt;(statementProxy, statementCallback, sqlRecognizer);
            <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> SELECT_FOR_UPDATE:
            executor = <span class="hljs-keyword">new</span> SelectForUpdateExecutor&lt;&gt;(statementProxy, statementCallback, sqlRecognizer);
            <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">default</span>:
            executor = <span class="hljs-keyword">new</span> PlainExecutor&lt;&gt;(statementProxy, statementCallback);
            <span class="hljs-keyword">break</span>;
    }
} <span class="hljs-keyword">else</span> {
    executor = <span class="hljs-keyword">new</span> MultiExecutor&lt;&gt;(statementProxy, statementCallback, sqlRecognizers);
}
</code></pre>
<p>主要流程就是： 先执行业务SQL，然后执行ConnectionProxy的commit方法，在这个方法中，会先帮我们执行对应的 undo_log SQL，然后提交事务</p>
<pre><code>PreparedStatementProxy#executeUpdate =&gt; 
ExecuteTemplate#execute =&gt; 
BaseTransactionalExecutor#execute =&gt;
AbstractDMLBaseExecutor#doExecute =&gt;
AbstractDMLBaseExecutor#executeAutoCommitTrue =&gt; 
AbstractDMLBaseExecutor#executeAutoCommitFalse =&gt; 在这一步操中，会触发statementCallback#execute方法，即调用调用原生PreparedStatement#executeUpdate方法
ConnectionProxy#commit
ConnectionProxy#processGlobalTransactionCommit
</code></pre>
<h2>UNDO_LOG插入</h2>
<pre><code class="language-java"><span class="hljs-comment">// ConnectionProxy#processGlobalTransactionCommit</span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">processGlobalTransactionCommit</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> SQLException </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// 注册分支事务，简单理解向server发一个请求，然后server在branch_table表里插入一条记录，不关注</span>
        register();
    } <span class="hljs-keyword">catch</span> (TransactionException e) {
        <span class="hljs-comment">// 如果没有for update 的sql,会直接在commit之前做注册,此时不止插入一条branch记录,而会附带锁信息进行竞争,下方的异常一般就是在注册时没拿到锁抛出,一般就是纯update语句的并发下会触发竞争锁失败的异常 @FUNKYE</span>
        recognizeLockKeyConflictException(e, context.buildLockKeys());
    }
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// undo_log处理，期望用 targetConnection 处理           @1</span>
        UndoLogManagerFactory.getUndoLogManager(<span class="hljs-keyword">this</span>.getDbType()).flushUndoLogs(<span class="hljs-keyword">this</span>);

        <span class="hljs-comment">// 提交本地事务，期望用 targetConnection 处理             @2</span>
        targetConnection.commit();
    } <span class="hljs-keyword">catch</span> (Throwable ex) {
        LOGGER.error(<span class="hljs-string">"process connectionProxy commit error: {}"</span>, ex.getMessage(), ex);
        report(<span class="hljs-keyword">false</span>);
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> SQLException(ex);
    }
    <span class="hljs-keyword">if</span> (IS_REPORT_SUCCESS_ENABLE) {
        report(<span class="hljs-keyword">true</span>);
    }
    context.reset();
}
</code></pre>
<ol>
<li>undo_log处理@1，解析当前事务分支涉及到的<code>undo_log</code>，然后使用<code>TargetConnection</code>， 写到数据库</li>
</ol>
<pre><code class="language-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">flushUndoLogs</span><span class="hljs-params">(ConnectionProxy cp)</span> <span class="hljs-keyword">throws</span> SQLException </span>{
    ConnectionContext connectionContext = cp.getContext();
    <span class="hljs-keyword">if</span> (!connectionContext.hasUndoLog()) {
        <span class="hljs-keyword">return</span>;
    }

    String xid = connectionContext.getXid();
    <span class="hljs-keyword">long</span> branchId = connectionContext.getBranchId();

    BranchUndoLog branchUndoLog = <span class="hljs-keyword">new</span> BranchUndoLog();
    branchUndoLog.setXid(xid);
    branchUndoLog.setBranchId(branchId);
    branchUndoLog.setSqlUndoLogs(connectionContext.getUndoItems());

    UndoLogParser parser = UndoLogParserFactory.getInstance();
    <span class="hljs-keyword">byte</span>[] undoLogContent = parser.encode(branchUndoLog);

    <span class="hljs-keyword">if</span> (LOGGER.isDebugEnabled()) {
        LOGGER.debug(<span class="hljs-string">"Flushing UNDO LOG: {}"</span>, <span class="hljs-keyword">new</span> String(undoLogContent, Constants.DEFAULT_CHARSET));
    }

    insertUndoLogWithNormal(xid, branchId, buildContext(parser.getName()), undoLogContent,cp.getTargetConnection());
}
</code></pre>
<ol start="2">
<li>提交本地事务@2，即通过<code>TargetConnection</code>提交事务。即 <code>务SQL执行</code>、<code>undo_log写入</code>、<code>即事务提交</code> 用的都是同一个<code>TargetConnection</code></li>
</ol>
<blockquote>
<p>lcn的内置数据库方案,lcn是将undolog写到他内嵌的h2(忘了是不是这个来着了)数据库上,此时会变成2个本地事务,一个是h2的undolog插入事务,一个是业务数据库的事务,如果在h2插入后,业务数据库异常,lcn的方案就会出现数据冗余,回滚数据的时候也是一样,删除undolog跟回滚业务数据不是一个本地事务.
但是lcn这样的好处就是入侵小,不需要另外添加undolog表。 感谢@FUNKYE大佬给的建议，对lcn不太了解，有机会好好研究一下</p>
</blockquote>
<h1>分支事务回滚</h1>
<ol>
<li>Server端向Client端发送回滚请求</li>
<li>Client端接收到Server发过来的请求，经过一系列处理，最终会到<code>DataSourceManager#branchRollback</code>方法</li>
<li>先根据resourceId从<code>DataSourceManager.dataSourceCache</code>中获取对应的<code>DataSourceProxy</code>，此时为<code>masterSlaveProxy</code>(回滚阶段我们就不考代理数据源问题，简单直接一些，反正最终拿到的都是<code>TragetConnection</code>)</li>
<li>根据Server端发过来的xid和branchId查找对应的undo_log并解析其<code>rollback_info</code>属性，每条undo_log可能会解析出多条<code>SQLUndoLog</code>,每个<code>SQLUndoLog</code>可以理解成是一个操作。比如一个分支事务先更新A表，再更新B表，这时候针对该分支事务生成的undo_log就包含两个<code>SQLUndoLog</code>：第一个<code>SQLUndoLog</code>对应的是更新A表的前后快照；第二个<code>SQLUndoLog</code>对应的是更新B表的前后快照</li>
<li>针对每条<code>SQLUndoLog</code>执行对应的回滚操作，比如一个<code>SQLUndoLog</code>对应的操作是<code>INSERT</code>，则其对应的回滚操作就是<code>DELETE</code></li>
<li>根据xid和branchId删除该undo_log</li>
</ol>
<pre><code class="language-java"><span class="hljs-comment">// AbstractUndoLogManager#undo   删除了部分非关键代码</span>

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">undo</span><span class="hljs-params">(DataSourceProxy dataSourceProxy, String xid, <span class="hljs-keyword">long</span> branchId)</span> <span class="hljs-keyword">throws</span> TransactionException </span>{
    Connection conn = <span class="hljs-keyword">null</span>;
    ResultSet rs = <span class="hljs-keyword">null</span>;
    PreparedStatement selectPST = <span class="hljs-keyword">null</span>;
    <span class="hljs-keyword">boolean</span> originalAutoCommit = <span class="hljs-keyword">true</span>;

    <span class="hljs-keyword">for</span> (; ; ) {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-comment">// 获取原生数据源的Connection, 回滚阶段我们不管代理数据源问题，最终拿到的都是 TargetConnection</span>
            conn = dataSourceProxy.getPlainConnection();

            <span class="hljs-comment">// 将回滚操作放在一个本地事务中，手动提交，确保最终业务SQL操作和undo_log删除操作一起提交</span>
            <span class="hljs-keyword">if</span> (originalAutoCommit = conn.getAutoCommit()) {
                conn.setAutoCommit(<span class="hljs-keyword">false</span>);
            }

            <span class="hljs-comment">// 根据xid 和 branchId 查询 undo_log，注意此时的SQL语句  SELECT * FROM undo_log WHERE branch_id = ? AND xid = ? FOR UPDATE</span>
            selectPST = conn.prepareStatement(SELECT_UNDO_LOG_SQL);
            selectPST.setLong(<span class="hljs-number">1</span>, branchId);
            selectPST.setString(<span class="hljs-number">2</span>, xid);
            rs = selectPST.executeQuery();

            <span class="hljs-keyword">boolean</span> exists = <span class="hljs-keyword">false</span>;
            <span class="hljs-keyword">while</span> (rs.next()) {
                exists = <span class="hljs-keyword">true</span>;
                <span class="hljs-comment">// status == 1 undo_log不处理，和防悬挂相关</span>
                <span class="hljs-keyword">if</span> (!canUndo(state)) {
                    <span class="hljs-keyword">return</span>;
                }

                <span class="hljs-comment">// 解析undo_log</span>
                <span class="hljs-keyword">byte</span>[] rollbackInfo = getRollbackInfo(rs);
                BranchUndoLog branchUndoLog = UndoLogParserFactory.getInstance(serializer).decode(rollbackInfo);
                <span class="hljs-keyword">try</span> {
                    setCurrentSerializer(parser.getName());
                    List&lt;SQLUndoLog&gt; sqlUndoLogs = branchUndoLog.getSqlUndoLogs();
                    <span class="hljs-keyword">if</span> (sqlUndoLogs.size() &gt; <span class="hljs-number">1</span>) {
                        Collections.reverse(sqlUndoLogs);
                    }
                    <span class="hljs-keyword">for</span> (SQLUndoLog sqlUndoLog : sqlUndoLogs) {
                        AbstractUndoExecutor undoExecutor = UndoExecutorFactory.getUndoExecutor(dataSourceProxy.getDbType(), sqlUndoLog);
                        <span class="hljs-comment">// 执行对应的回滚操作</span>
                        undoExecutor.executeOn(conn);
                    }
                } 
            }

            <span class="hljs-comment">// </span>
            <span class="hljs-keyword">if</span> (exists) {
                LOGGER.error(<span class="hljs-string">"\n delete from undo_log where xid={} AND branchId={} \n"</span>, xid, branchId);
                deleteUndoLog(xid, branchId, conn);
                conn.commit();
            <span class="hljs-comment">// 和防悬挂相关 如果根据 xid和branchId 没有查到undo_log，说明这个分支事务有异常：例如业务处理超时，导致全局事务回滚，但这时候业务undo_log并没有插入</span>
            } <span class="hljs-keyword">else</span> {
                LOGGER.error(<span class="hljs-string">"\n insert into undo_log xid={},branchId={} \n"</span>, xid, branchId);
                insertUndoLogWithGlobalFinished(xid, branchId, UndoLogParserFactory.getInstance(), conn);
                conn.commit();
            }
            <span class="hljs-keyword">return</span>;
        } <span class="hljs-keyword">catch</span> (Throwable e) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> BranchTransactionException(BranchRollbackFailed_Retriable, String
                .format(<span class="hljs-string">"Branch session rollback failed and try again later xid = %s branchId = %s %s"</span>, xid,branchId, e.getMessage()), e);
        }
    }
}
</code></pre>
<p>有以下几个注意点：</p>
<ol>
<li>回滚时不考虑数据源代理问题，最终都是使用<code>TargetConnection</code></li>
<li>设置atuoCommit为false，即需要手动提交事务</li>
<li>根据xid和branchId查询undo_log时加了<code>for update</code>，也就是说，这个事务会持有这条undo_log的锁直到所有回滚操作都完成，因为完成之后才会</li>
</ol>
<h1>多层代理问题</h1>
<p>数据源多层代理会导致的几个问题在文章开头的时候已经提到过了，重点分析一下为什么会造成以上问题：</p>
<h2>对分支事务提交的影响</h2>
<p>先分析一下，如果使用双层代理会发生什么？我们从两个方面来分析：<code>业务SQL</code>和<code>undo_log</code></p>
<ol>
<li>业务SQL</li>
</ol>
<pre><code>PreparedStatementProxy1.executeUpdate =&gt; 
statementCallback#executeUpdate(PreparedStatementProxy2#executeUpdate) =&gt; 
PreparedStatement#executeUpdate
</code></pre>
<p>好像没啥影响，就是多绕了一圈，最终还是通过<code>PreparedStatement</code>执行</p>
<ol start="2">
<li>undo_log</li>
</ol>
<pre><code>ConnectionProxy1#getTargetConnection -&gt; 
ConnectionProxy2#prepareStatement -&gt; 
PreparedStatementProxy2#executeUpdate -&gt; 
PreparedStatement#executeUpdate(原生的undo_log写入，在此之前会对为该 undo_log 生成 undo_log2(即 undo_log 的 undo_log)) -&gt;
ConnectionProxy2#commit -&gt; 
ConnectionProxy2#processGlobalTransactionCommit(写入undo_log2) -&gt;
ConnectionProxy2#getTargetConnection -&gt;
TargetConnection#prepareStatement -&gt;
PreparedStatement#executeUpdate
</code></pre>
<h2>对分支事务回滚的影响</h2>
<blockquote>
<p>在事务回滚之后，为何undo_log没有被删除呢？</p>
</blockquote>
<p>其实并不是没有被删除。前面已经说过，双层代理会导致<code>undo_log</code>被当作分支事务来处理，所以也会为该 <code>undo_log</code>生成一个undo_log(假设为<code>undo_log2</code>),而<code>undo_log2</code>生成的有问题(其实也没问题，就应该这样生成)，从而导致回滚时会将<code>业务表关联的undo_log</code>也一起删除，最终导致<code>业务表对应的事务分支</code>回滚时发现undo_log不存在，从而又多生成一条状态为为1的undo_log</p>
<p><strong>回滚之前</strong></p>
<pre><code>// undo_log
84	59734070967644161	172.16.120.59:23004:59734061438185472 serializer=jackson 1.1KB  0
85	59734075254222849	172.16.120.59:23004:59734061438185472 serializer=jackson 4.0KB  0

// branch_table
59734070967644161	172.16.120.59:23004:59734061438185472		jdbc:mysql://172.16.248.10:3306/tuya_middleware
59734075254222849	172.16.120.59:23004:59734061438185472		jdbc:mysql://172.16.248.10:3306/tuya_middleware

// lock_table
jdbc:mysql://xx^^^seata_storage^^^1 59734070967644161	jdbc:mysql://172.16.248.10:3306/tuya_middleware	seata_storage	  1
jdbc:mysql://xx^^^undo_log^^^84	    59734075254222849	jdbc:mysql://172.16.248.10:3306/tuya_middleware	undo_log	      84
</code></pre>
<p><strong>回滚之后</strong></p>
<pre><code>// 生成了一条状态为1的undo_log，对应的日志为: undo_log added with GlobalFinished
86	59734070967644161	172.16.120.59:23004:59734061438185472 serializer=jackson 1.0Byte  1
</code></pre>
<h3>问题分析</h3>
<ol>
<li>根据xid和branchId找到对应的undo_log日志</li>
<li>对undo_log进行解析，主要就是解析它的<code>rollback_info</code>字段，<code>rollback_info</code>解析出来就是一个<code>SQLUndoLog集合</code>，每条<code>SQLUndoLog</code>对应着一个操作，里面包含了该操作的前后的快照，然后执行对应的回滚</li>
<li>根据xid和branchId删除undo_log日志</li>
</ol>
<p>因为双层代理问题，导致一条undo_log变成了一个分支事务，所以发生回滚时，我们也需要对undo_log分支事务进行回滚：
1、首先根据xid和branchId找到对应的<code>undo_log</code>并解析其<code>rollback_info</code>属性，这里解析出来的rollback_info包含了两条<code>SQLUndoLog</code>。为什么有两条？</p>
<blockquote>
<p>仔细想想也可以可以理解，第一层代理针对<code>seata_storage</code>的操作，放到缓存中，本来执行完之后是需要清掉的，但因为这里是双层代理，所以这时候这个流程并没有结束。轮到第二层代理对<code>undo_log</code>操作时，将该操作放到缓存中，此时缓存中有两个操作，分别为<code>seata_storage的UPDATE</code> 和 <code>undo_log的INSERT</code>。所以这也就很好理解为什么针对<code>undo_log操作</code>的那条undo_log格外大(4KB)，因为它的<code>rollback_info</code>包含了两个操作。</p>
</blockquote>
<p>有一点需要注意的是，第一条<code>SQLUndoLog</code>对应的after快照，里面的branchId=<code>59734070967644161</code> pk=<code>84</code>， 即 <code>seata_storage分支对应的branchId</code>  和 <code>seata_storage对应的undo_log PK</code>。也就是说，undo_log回滚时候 把<code>seata_storage对应的undo_log</code>删掉了。
那undo_log本身对应的undo_log 如何删除呢？在接下来的逻辑中会根据xid和branchId删除</p>
<p>2、解析第一条<code>SQLUndoLog</code>，此时对应的是<code>undo_log的INSERT</code>操作，所以其对应的回滚操作是<code>DELETE</code>。因为<code>undo_log</code>此时被当作了业务表。所以这一步会将<code>59734075254222849</code>对应的undo_log删除，<strong>但这个其实是业务表对应的对应的<code>undo_log</code></strong></p>
<p>3、解析第二条<code>SQLUndoLog</code>，此时对应的是<code>seata_storage的UPDATE</code>操作，这时会通过快照将<code>seata_storage</code>对应的记录恢复</p>
<p>4、根据xid和branchId删除undo_log日志，这里删除的是<code>undo_log 的 undo_log , 即 undo_log2</code>。所以，执行到这里，两条undo_log就已经被删除了</p>
<p>5、接下来回滚<code>seata_storage</code>，因为这时候它对应的undo_log已经在步骤2删掉了，所以此时查不到undo_log，然后重新生成一条<code>status == 1 的 undo_log</code></p>
<h1>案例分析</h1>
<h2>背景</h2>
<p>1、配置了三个数据源: 两个物理数据源、一个逻辑数据源，但是两个物理数据源对应的连接地址是一样的。这样做有意思吗？</p>
<pre><code>@Bean(&quot;dsMaster&quot;)
DynamicDataSource dsMaster() {
    return new DynamicDataSource(masterDsRoute);
}

@Bean(&quot;dsSlave&quot;)
DynamicDataSource dsSlave() {
    return new DynamicDataSource(slaveDsRoute);
}

@Primary
@Bean(&quot;masterSlave&quot;)
DataSource masterSlave(@Qualifier(&quot;dsMaster&quot;) DataSource dataSourceMaster,
                        @Qualifier(&quot;dsSlave&quot;) DataSource dataSourceSlave) throws SQLException {
    Map&lt;String, DataSource&gt; dataSourceMap = new HashMap&lt;&gt;(2);
    //主库
    dataSourceMap.put(&quot;dsMaster&quot;, dataSourceMaster);
    //从库
    dataSourceMap.put(&quot;dsSlave&quot;, dataSourceSlave);
    // 配置读写分离规则
    MasterSlaveRuleConfiguration masterSlaveRuleConfig = new MasterSlaveRuleConfiguration(
            &quot;masterSlave&quot;, &quot;dsMaster&quot;, Lists.newArrayList(&quot;dsSlave&quot;)
    );
    Properties shardingProperties = new Properties();
    shardingProperties.setProperty(&quot;sql.show&quot;, &quot;true&quot;);
    shardingProperties.setProperty(&quot;sql.simple&quot;, &quot;true&quot;);
    // 获取数据源对象
    DataSource dataSource = MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, masterSlaveRuleConfig, shardingProperties);
    log.info(&quot;datasource initialized!&quot;);
    return dataSource;˚
}
</code></pre>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3e05c8fc0294a8caf4d0883a4676750~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<p>2、开启seata的数据源动态代理，根据seata的数据源代理逻辑可以知道，最终会生成三个代理数据源，原生数据源和代理数据源的关系缓存在<code>DataSourceProxyHolder.dataSourceProxyMap</code>中，假如原生数据源和代理数据源对应的关系如下：</p>
<pre><code>dsMaster(DynamicDataSource)           =&gt;       dsMasterProxy(DataSourceProxy)
dsSlave(DynamicDataSource)           =&gt;       dsSlaveProxy(DataSourceProxy)
masterSlave(MasterSlaveDataSource)       =&gt;       masterSlaveProxy(DataSourceProxy)
</code></pre>
<p>所以，最终在IOC容器中存在的数据源是这三个： dsMasterProxy 、 dsSlaveProxy 、 masterSlaveProxy 。根据@Primary的特性可以知道，当我们从容器中获取一个DataSource的时候，默认返回的就是代理数据源 masterSlaveProxy</p>
<blockquote>
<p>对shardingjdbc没有具体的研究过，只是根据debug时看到的代码猜测它的工作机制，又不对的地方，还请大佬指出来</p>
</blockquote>
<p><code>masterSlaveProxy</code>可以看成是<code>被 DataSourceProxy 包装后的 MasterSlaveDataSource</code>。我们可以大胆的猜测<code>MasterSlaveDataSource</code>并不是一个物理数据源，而是一个逻辑数据源，可以简单的认为里面包含了路由的逻辑。当我们获取一个连接时，会通过里面的路由规则选择到具体的物理数据源，然后通过该物理数据源获取一个真实的连接。
路由规则应该可以自己定义，根据debug时观察到的现象，默认的路由规则应该是：</p>
<ol>
<li>针对select 读操作，会路由到从库，即我们的 dsSlave</li>
<li>针对update 写操作，会路由到主库，即我们的 dsMaster</li>
</ol>
<p>3、每个DataSourceProxy在初始化的时候，会解析该真实DataSource的连接地址，然后将该<code>连接地址和DataSourceProxy本身</code>维护<code>DataSourceManager.dataSourceCache</code>中。<code>DataSourceManager.dataSourceCache</code>有一个作用是用于回滚：回滚时根据连接地址找到对应的<code>DataSourceProxy</code>,然后基于该<code>DataSourceProxy</code>做回滚操作。
但我们可以发现这个问题，这三个数据源解析出来的连接地址是一样的，也就是key重复了，所以在<code>DataSourceManager.dataSourceCache</code>中中，当连接地相同时，后注册的数据源会覆盖已存在的。即： <code>DataSourceManager.dataSourceCache</code>最终存在的是<code>masterSlaveProxy</code>,也就是说，最终会通过<code>masterSlaveProxy</code>进行回滚，这点很重要。</p>
<p>4、涉及到的表：很简单，我们期待的就一个业务表<code>seata_account</code>，但因为重复代理问题，导致seata将undo_log也当成了一个业务表</p>
<ol>
<li>seata_account</li>
<li>undo_log</li>
</ol>
<p>好了，这里简单介绍一下背景，接下来进入Seata环节</p>
<h2>需求</h2>
<p>我们的需求很简单，就是在分支事务里面执行一条简单的update操作，更新<code>seata_account</code>的count值。在更新完之后，手动抛出一个异常，触发全局事务的回滚。
为了更便于排查问题，减少干扰，我们全局事务中就使用一个分支事务，没有其它分支事务了。SQL如下:</p>
<pre><code>update seata_account set count = count - 1 where id = 100;
</code></pre>
<h2>问题现象</h2>
<p>Client：在控制台日志中，不断重复打印以下日志</p>
<ol>
<li>以上日志打印的间隔为20s，而我查看了数据库的<code>innodb_lock_wait_timeout</code>属性值，刚好就是20，说明每次回滚请求过来的时候，都因为获取锁超时(20)而回滚失败</li>
<li>为什么会没过20s打印一次？因为Server端会有定时处理回滚请求</li>
</ol>
<pre><code>// 分支事务开始回滚
Branch rollback start: 172.16.120.59:23004:59991911632711680 59991915571163137 jdbc:mysql://172.16.248.10:3306/tuya_middleware

// undo_log事务分支 原始操作对应是 insert, 所以其回滚为 delete
undoSQL undoSQL=DELETE FROM undo_log WHERE id = ?  ， PK=[[id,139]] 
// 因为第一层代理对应的操作也在上下文中，undo_log分支事务 提交时候， 对应的undo_log包含两个操作
undoSQL undoSQL=UPDATE seata_account SET money = ? WHERE id = ?  ， PK=[[id,1]] 

// 该分支事务回滚完成之后，再删除该分支事务的对应的 undo_log
delete from undo_log where xid=172.16.120.59:23004:59991911632711680 AND branchId=59991915571163137 

// 抛出异常，提示回滚失败，失败原因是`Lock wait timeout exceeded`， 在根据xid和branchId删除undo_log时失败，失败原因是获取锁超时，说明此时有另一个操作持有该记录的锁没有释放
branchRollback failed. branchType:[AT], xid:[172.16.120.59:23004:59991911632711680], branchId:[59991915571163137], resourceId:[jdbc:mysql://172.16.248.10:3306/tuya_middleware], applicationData:[null]. reason:[Branch session rollback failed and try again later xid = 172.16.120.59:23004:59991911632711680 branchId = 59991915571163137 Lock wait timeout exceeded; try restarting transaction]
</code></pre>
<p>Server：每20s打印以下日志，说明server在不断的重试发送回滚请求</p>
<pre><code>Rollback branch transaction fail and will retry, xid = 172.16.120.59:23004:59991911632711680 branchId = 59991915571163137
</code></pre>
<p>在该过程中，涉及到的SQL大概如下：</p>
<pre><code class="language-sql">1. <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> undo_log <span class="hljs-keyword">WHERE</span> branch_id = ? <span class="hljs-keyword">AND</span> xid = ? <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">UPDATE</span>							slaveDS
<span class="hljs-number">2.</span> <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> undo_log <span class="hljs-keyword">WHERE</span>  (<span class="hljs-keyword">id</span> ) <span class="hljs-keyword">in</span> (  (?)  )												        slaveDS
<span class="hljs-number">3.</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> undo_log <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = ?  															              masterDS
<span class="hljs-number">4.</span> <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> seata_account <span class="hljs-keyword">WHERE</span>  (<span class="hljs-keyword">id</span> ) <span class="hljs-keyword">in</span> (  (?)  )										      masterDS
<span class="hljs-number">5.</span> <span class="hljs-keyword">UPDATE</span> seata_account <span class="hljs-keyword">SET</span> money = ? <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = ?  											        masterDS
<span class="hljs-number">6.</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> undo_log <span class="hljs-keyword">WHERE</span> branch_id = ? <span class="hljs-keyword">AND</span> xid = ?											      masterDS
</code></pre>
<p>此时查看数据库的 事务情况、锁情况 、锁等待关系
1、查当前正在执行的事务</p>
<pre><code class="language-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> information_schema.INNODB_TRX;
</code></pre>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1d9852b91a9949f781e1f90bffe95fbf~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<p>2、查当前锁情况</p>
<pre><code class="language-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> information_schema.INNODB_LOCKs;
</code></pre>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1a29748a3af34e7c90e3aa7cb78564bc~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<p>3、查当前锁等待关系</p>
<pre><code class="language-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> information_schema.INNODB_LOCK_waits;

<span class="hljs-keyword">SELECT</span>
	block_trx.trx_mysql_thread_id <span class="hljs-keyword">AS</span> 已经持有锁的sessionID,
	request_trx.trx_mysql_thread_id <span class="hljs-keyword">AS</span> 正在申请锁的sessionID,
	block_trx.trx_query <span class="hljs-keyword">AS</span> 已经持有锁的<span class="hljs-keyword">SQL</span>语句,
	request_trx.trx_query <span class="hljs-keyword">AS</span> 正在申请锁的<span class="hljs-keyword">SQL</span>语句,
	waits.blocking_trx_id <span class="hljs-keyword">AS</span> 已经持有锁的事务<span class="hljs-keyword">ID</span>,
	waits.requesting_trx_id <span class="hljs-keyword">AS</span> 正在申请锁的事务<span class="hljs-keyword">ID</span>,
	waits.requested_lock_id <span class="hljs-keyword">AS</span> 锁对象的<span class="hljs-keyword">ID</span>,
	locks.lock_table <span class="hljs-keyword">AS</span> lock_table, 					<span class="hljs-comment">-- 锁对象所锁定的表</span>
	locks.lock_type <span class="hljs-keyword">AS</span> lock_type, 						<span class="hljs-comment">-- 锁类型</span>
	locks.lock_mode <span class="hljs-keyword">AS</span> lock_mode 							<span class="hljs-comment">-- 锁模式</span>
<span class="hljs-keyword">FROM</span>
	information_schema.innodb_lock_waits <span class="hljs-keyword">AS</span> waits
	<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> information_schema.innodb_trx <span class="hljs-keyword">AS</span> block_trx <span class="hljs-keyword">ON</span> waits.blocking_trx_id = block_trx.trx_id
	<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> information_schema.innodb_trx <span class="hljs-keyword">AS</span> request_trx <span class="hljs-keyword">ON</span> waits.requesting_trx_id = request_trx.trx_id
	<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> information_schema.innodb_locks <span class="hljs-keyword">AS</span> locks <span class="hljs-keyword">ON</span> waits.requested_lock_id = locks.lock_id;
</code></pre>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ca5b50cab534a69a49c3e470518e3b6~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<ol>
<li>涉及到到记录为 <code>branch_id = 59991915571163137 AND xid = 172.16.120.59:23004:59991911632711680</code></li>
<li>事务ID<code>1539483284</code>持有该记录的锁，但是它对应的SQL为空，那应该是在等待commit</li>
<li>事务ID<code>1539483286</code>在尝试获取该记录的锁，但从日志可以发现，它一直锁等待超时</li>
</ol>
<p>大概可以猜测是 <code>select for update</code> 和 <code>delete from undo ...</code> 发生了冲突。根据代码中的逻辑，这两个操作应该是放在一个事务中提交了，为什么被分开到两个事务了？</p>
<h2>问题分析</h2>
<p>结合上面的介绍的回滚流程看看我们这个例子在回滚时会发生什么</p>
<ol>
<li>先获取数据源，此时dataSourceProxy.getPlainConnection()获取到的是<code>MasterSlaveDataSource</code>数据源</li>
<li>在<code>select for update</code>操作的时候，通过<code>MasterSlaveDataSource</code>获取一个<code>Connection</code>，前面说到过<code>MasterSlaveDataSource</code>是一个逻辑数据源，里面有路由逻辑，根据上面介绍的，这时候拿到的是<code>dsSlave</code>的<code>Connection</code></li>
<li>在执行<code>delete from undo ...</code>操作的时候，这时候拿到的是<code>dsMaster</code>的<code>Connection</code></li>
<li>虽然<code>dsSlave</code>和<code>dsMaster</code>对应的是相同的地址，但此时获取到的肯定是不同的连接，所以此时两个操作肯定是分布在两个事务中</li>
<li>执行<code>select for update</code>的事务，会一直等待直到删除undo_log完成才会提交</li>
<li>执行<code>delete from undo ...</code>的事务，会一直等待<code>select for update</code>的事务释放锁</li>
<li>典型的死锁问题</li>
</ol>
<h2>验证猜想</h2>
<p>我尝试用了两个方法验证这个问题：</p>
<ol>
<li>
<p>修改Seata代码，将<code>select for update</code>改成<code>select</code>，此时在查询undo_log就不需要持有该记录的锁，也就不会造成死锁</p>
</li>
<li>
<p>修改数据源代理逻辑，这才是问题的关键，该问题主要原因不是<code>select for update</code>。在此之前多层代理问题已经产生，然后才会造成死锁问题。从头到尾我们就不应该对<code>masterSlave</code>数据源进行代理。它只是一个逻辑数据源，为什么要对它进行代理呢？如果代理<code>masterSlave</code>，就不会造成多层代理问题，也就不会造成删除undo_log时的死锁问题</p>
</li>
</ol>
<h2>最终实现</h2>
<p><code>masterSlave</code>也是一个<code>DataSource</code>类型，该如何仅仅对<code>dsMaster</code> 和 <code>dsSlave</code> 代理而不对<code>masterSlave</code>代理呢？观察<code>SeataAutoDataSourceProxyCreator#shouldSkip</code>方法，我们可以通过EnableAutoDataSourceProxy注解的<code>excludes</code>属性解决这个问题</p>
<pre><code>@Override
protected boolean shouldSkip(Class&lt;?&gt; beanClass, String beanName) {
    return SeataProxy.class.isAssignableFrom(beanClass) ||
            DataSourceProxy.class.isAssignableFrom(beanClass) ||
            !DataSource.class.isAssignableFrom(beanClass) ||
            Arrays.asList(excludes).contains(beanClass.getName());
}
</code></pre>
<p>即: 将数据源自动代理关闭，然后在启动类加上这个注解</p>
<pre><code>@EnableAutoDataSourceProxy(excludes = {&quot;org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.MasterSlaveDataSource&quot;})
</code></pre>
<h1>自动代理在新版本中的优化</h1>
<p>因为<code>Seata 1.4.0</code>还没有正式发布，我目前看的是<code>1.4.0-SNAPSHOT</code>版本的代码，即当前时间<code>ddevelop</code>分支最新的代码</p>
<h2>代码改动</h2>
<p>主要改动如下，一些小的细节就不过多说明了：</p>
<ol>
<li><code>DataSourceProxyHolder</code>调整</li>
<li><code>DataSourceProxy</code>调整</li>
<li><code>SeataDataSourceBeanPostProcessor</code>新增</li>
</ol>
<h3>DataSourceProxyHolder</h3>
<p>在这个类改动中，最主要是其<code>putDataSource</code>方法的改动</p>
<pre><code class="language-java"><span class="hljs-function"><span class="hljs-keyword">public</span> SeataDataSourceProxy <span class="hljs-title">putDataSource</span><span class="hljs-params">(DataSource dataSource, BranchType dataSourceProxyMode)</span> </span>{
    DataSource originalDataSource;
    <span class="hljs-keyword">if</span> (dataSource <span class="hljs-keyword">instanceof</span> SeataDataSourceProxy) {
        SeataDataSourceProxy dataSourceProxy = (SeataDataSourceProxy) dataSource;
        <span class="hljs-comment">// 如果是代理数据源，并且和当前应用配置的数据源代理模式(AT/XA)一样, 则直接返回</span>
        <span class="hljs-keyword">if</span> (dataSourceProxyMode == dataSourceProxy.getBranchType()) {
            <span class="hljs-keyword">return</span> (SeataDataSourceProxy)dataSource;
        }

        <span class="hljs-comment">// 如果是代理数据源，和当前应用配置的数据源代理模式(AT/XA)不一样，则需要获取其TargetDataSource,然后为其创建一个代理数据源</span>
        originalDataSource = dataSourceProxy.getTargetDataSource();
    } <span class="hljs-keyword">else</span> {
        originalDataSource = dataSource;
    }

    <span class="hljs-comment">// 如果有必要，基于 TargetDataSource 创建 代理数据源</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.dataSourceProxyMap.computeIfAbsent(originalDataSource,
            BranchType.XA == dataSourceProxyMode ? DataSourceProxyXA::<span class="hljs-keyword">new</span> : DataSourceProxy::<span class="hljs-keyword">new</span>);
}
</code></pre>
<p><code>DataSourceProxyHolder#putDataSource</code>方法主要在两个地方被用到：一个是在<code>SeataAutoDataSourceProxyAdvice</code>切面中；一个是在<code>SeataDataSourceBeanPostProcessor</code>中。
这段判断为我们解决了什么问题？数据源多层代理问题。在开启了数据源自动代理的前提下，思考以下场景：</p>
<ol>
<li>如果我们在项目中手动注入了一个<code>DataSourceProxy</code>，这时候在切面调用<code>DataSourceProxyHolder#putDataSource</code>方法时会直接返回该<code>DataSourceProxy</code>本身，而不会为其再创建一个<code>DataSourceProxy</code></li>
<li>如果我们在项目中手动注入了一个<code>DruidSource</code>，这时候在切面调用<code>DataSourceProxyHolder#putDataSource</code>方法时会为其再创建一个<code>DataSourceProxy</code>并返回</li>
</ol>
<p>这样看好像问题已经解决了，有没有可能会有其它的问题呢？看看下面的代码</p>
<pre><code class="language-java"><span class="hljs-meta">@Bean</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> DataSourceProxy <span class="hljs-title">dsA</span><span class="hljs-params">()</span></span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> DataSourceProxy(druidA)
}

<span class="hljs-meta">@Bean</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> DataSourceProxy <span class="hljs-title">dsB</span><span class="hljs-params">(DataSourceProxy dsA)</span></span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> DataSourceProxy(dsA)
}
</code></pre>
<ol>
<li>这样写肯定是不对，但如果他就要这样写你也没办法</li>
<li><code>dsA</code>没什么问题，但<code>dsB</code>还是会产生双层代理的问题，因为此时<code>dsB 的 TargetDataSource</code>是<code>dsA</code></li>
<li>这就涉及到<code>DataSourceProxy</code>的改动</li>
</ol>
<h3>DataSourceProxy</h3>
<pre><code class="language-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">DataSourceProxy</span><span class="hljs-params">(DataSource targetDataSource, String resourceGroupId)</span> </span>{
    <span class="hljs-comment">// 下面这个判断，保证了在我们传入一个DataSourceProxy的时候，也不会产生双层代理问题</span>
    <span class="hljs-keyword">if</span> (targetDataSource <span class="hljs-keyword">instanceof</span> SeataDataSourceProxy) {
        LOGGER.info(<span class="hljs-string">"Unwrap the target data source, because the type is: {}"</span>, targetDataSource.getClass().getName());
        targetDataSource = ((SeataDataSourceProxy) targetDataSource).getTargetDataSource();
    }
    <span class="hljs-keyword">this</span>.targetDataSource = targetDataSource;
    init(targetDataSource, resourceGroupId);
}
</code></pre>
<h3>SeataDataSourceBeanPostProcessor</h3>
<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SeataDataSourceBeanPostProcessor</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BeanPostProcessor</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger LOGGER = LoggerFactory.getLogger(SeataDataSourceBeanPostProcessor.class);

    ......

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">postProcessAfterInitialization</span><span class="hljs-params">(Object bean, String beanName)</span> <span class="hljs-keyword">throws</span> BeansException </span>{
        <span class="hljs-keyword">if</span> (bean <span class="hljs-keyword">instanceof</span> DataSource) {
            <span class="hljs-comment">//When not in the excludes, put and init proxy.</span>
            <span class="hljs-keyword">if</span> (!excludes.contains(bean.getClass().getName())) {
                <span class="hljs-comment">//Only put and init proxy, not return proxy.</span>
                DataSourceProxyHolder.get().putDataSource((DataSource) bean, dataSourceProxyMode);
            }

            <span class="hljs-comment">//If is SeataDataSourceProxy, return the original data source.</span>
            <span class="hljs-keyword">if</span> (bean <span class="hljs-keyword">instanceof</span> SeataDataSourceProxy) {
                LOGGER.info(<span class="hljs-string">"Unwrap the bean of the data source,"</span> +
                    <span class="hljs-string">" and return the original data source to replace the data source proxy."</span>);
                <span class="hljs-keyword">return</span> ((SeataDataSourceProxy) bean).getTargetDataSource();
            }
        }
        <span class="hljs-keyword">return</span> bean;
    }
}
</code></pre>
<ol>
<li><code>SeataDataSourceBeanPostProcessor</code>实现了<code>BeanPostProcessor</code>接口，在一个bean初始化后，会执行<code>BeanPostProcessor#postProcessAfterInitialization</code>方法。也就是说，在<code>postProcessAfterInitialization</code>方法中，这时候的bean已经是可用状态了</li>
<li>为什么要提供这么一个类呢？从它的代码上来看，仅仅是为了再bean初始化之后，为数据源初始化对应的<code>DataSourceProxy</code>，但为什么要这样做呢？</li>
</ol>
<blockquote>
<p>因为有些数据源在应用启动之后，可能并不会初始化(即不会调用数据源的相关方法)。如果没有提供<code>SeataDataSourceBeanPostProcessor</code>类，那么就只有在<code>SeataAutoDataSourceProxyAdvice</code>切面中才会触发<code>DataSourceProxyHolder#putDataSource</code>方法。假如有一个客户端在回滚的时候宕机了，在重启之后，Server端通过定时任务向其派发回滚请求，这时候客户端需要先根据<code>rsourceId</code>(连接地址)找到对应的<code>DatasourceProxy</code>。但如果在此之前客户端还没有主动触发数据源的相关方法，就不会进入<code>SeataAutoDataSourceProxyAdvice</code>切面逻辑，也就不会为该数据源初始化对应的<code>DataSourceProxy</code>，从而导致回滚失败</p>
</blockquote>
<h2>多层代理总结</h2>
<p>通过上面的分析，我们大概已经知道了seata在避免多层代理上的一些优化，但其实还有一个问题需要注意：<strong>逻辑数据源的代理</strong>
<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6910095aadab436eaffe03a752e44240~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<p>这时候的调用关系为： <code>masterSlaveProxy -&gt;　masterSlave -&gt; masterproxy/slaveProxy -&gt; master/slave</code></p>
<p>此时可以通过<code>excludes</code>属性排除逻辑数据源，从而不为其创建数据源代理。</p>
<p>总结一下：</p>
<ol>
<li>在为<code>数据源</code>初始化对应的<code>DataSourceProxy</code>时，判断是否有必要为其创建对应的<code>DataSourceProxy</code>，如果本身就是<code>DataSourceProxy</code>，就直接返回</li>
<li>针对一些<code>数据源</code>手动注入的情况，为了避免一些人为误操作的导致的多层代理问题，在<code>DataSourceProxy</code>构造函数中添加了判断，<code>如果入参TragetDatasource本身就是一个DataSourceProxy， 则获取其target属性作为新DataSourceProxy的tragetDatasource</code></li>
<li>针对一些其它情况，比如<strong>逻辑数据源代理问题</strong>，通过<code>excludes</code>属性添加排除项，这样可以避免为逻辑数据源创建<code>DataSourceProxy</code></li>
</ol>
<h1>全局事务和本地事务使用建议</h1>
<p>有一个问题，如果在一个方法里涉及到多个DB操作，比如涉及到3条update操作，我们需不需在这个方法使用spring中的<code>@Transactional</code>注解？针对这个问题，我们分别从两个角度考虑：不使用<code>@Transactional</code>注解 和 使用<code>@Transactional</code>注解</p>
<h2>不使用<code>@Transactional</code>注解</h2>
<ol>
<li>在提交阶段，因为该分支事务有3条update操作，每次执行update操作的时候，都会通过数据代理向TC注册一个分支事务，并为其生成对应的undo_log，最终3个update操作被当作3个分支事务来处理</li>
<li>在回滚阶段，需要回滚3个分支事务</li>
<li>数据的一致性通过seata全局事务来保证</li>
</ol>
<h2>使用<code>@Transactional</code>注解</h2>
<ol>
<li>在提交阶段，3个update操作被当作一个分支事务来提交，所以最终只会注册一个分支事务</li>
<li>在回滚阶段，需要回滚1个分支事务</li>
<li>数据的一致性：这3个update的操作通过本地事务的一致性保证；全局一致性由seata全局事务来保证。此时3个update仅仅是一个分支事务而已</li>
</ol>
<h2>结论</h2>
<p>通过上面的对比，答案是显而易见的，合理的使用本地事务，可以大大的提升全局事务的处理速度。上面仅仅是3个DB操作，如果一个方法里面涉及到的DB操作更多呢，这时候两种方式的差别是不是更大呢？</p>
<p>最后，感谢@FUNKYE大佬为我解答了很多问题并提供了宝贵建议！</p>
</section><footer class="footer-container"><div class="footer-body"><img src="//img.alicdn.com/tfs/TB1dGrSwVT7gK0jSZFpXXaTkpXa-4802-1285.png"/><p class="docsite-power">website powered by docsite</p><div class="cols-container"><div class="col col-12"><h3>愿景</h3><p>Seata 是一款阿里巴巴开源的分布式事务解决方案，致力于在微服务架构下提供高性能和简单易用的分布式事务服务。</p></div><div class="col col-6"><dl><dt>文档</dt><dd><a href="/zh-cn/docs/overview/what-is-seata.html" target="_self">Seata 是什么？</a></dd><dd><a href="/zh-cn/docs/user/quickstart.html" target="_self">快速开始</a></dd><dd><a href="https://github.com/seata/seata.github.io/issues/new" target="_self">报告文档问题</a></dd><dd><a href="https://github.com/seata/seata.github.io" target="_self">在Github上编辑此文档</a></dd></dl></div><div class="col col-6"><dl><dt>资源</dt><dd><a href="/zh-cn/blog/index.html" target="_self">博客</a></dd><dd><a href="/zh-cn/community/index.html" target="_self">社区</a></dd></dl></div></div><div class="copyright"><span>Copyright © 2019 Seata</span></div></div></footer></div></div>
	<script src="https://f.alicdn.com/react/15.4.1/react-with-addons.min.js"></script>
	<script src="https://f.alicdn.com/react/15.4.1/react-dom.min.js"></script>
	<script>
		window.rootPath = '';
  </script>
	<script src="/build/blogDetail.js"></script>
	<script>
    var _hmt = _hmt || [];
    (function() {
      var hm = document.createElement("script");
      hm.src = "https://hm.baidu.com/hm.js?104e73ef0c18b416b27abb23757ed8ee";
      var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(hm, s);
    })();
    </script>
</body>
</html>
